﻿//-----------------------------------------------------------------------
// <copyright file="DSUtility.cs" company="Studio AT s.r.l.">
//     Copyright (c) Studio AT s.r.l. All rights reserved.
// </copyright>
// <author>Nicogis</author>
//-----------------------------------------------------------------------

////TODO: sign the project (project properties > signing tab > sign the assembly)
////      this is strongly suggested if the dll will be registered using regasm.exe <your>.dll /codebase
namespace Studioat.ArcGis.Soe.Rest
{
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.Diagnostics.CodeAnalysis;
    using System.EnterpriseServices;
    using System.Globalization;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Text;
    using ESRI.ArcGIS.Carto;
    using ESRI.ArcGIS.esriSystem;
    using ESRI.ArcGIS.Geodatabase;
    using ESRI.ArcGIS.Geometry;
    using ESRI.ArcGIS.Location;
    using ESRI.ArcGIS.Server;
    using ESRI.ArcGIS.SOESupport;

    /// <summary>
    /// class Dynamic Segmentation Utility
    /// </summary>
    [ComVisible(true)]
    [Guid("dd651114-5fb4-47b3-93ce-9b3ab6cb2849")]    
    [ClassInterface(ClassInterfaceType.None)]
    [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1306:FieldNamesMustBeginWithLowerCaseLetter", Justification = "Warning FxCop - Error Code ESRI - Capabilities")]
    [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Warning FxCop - Error Code ESRI - pSOH")]
    public class DSUtility : ServicedComponent, IServerObjectExtension, IObjectConstruct, IRESTRequestHandler
    {
        /// <summary>
        /// name of soe
        /// </summary>
        private string soeName;

        /// <summary>
        /// Helper ServerObject
        /// </summary>
        private IServerObjectHelper serverObjectHelper;

        /// <summary>
        /// request handler
        /// </summary>
        private IRESTRequestHandler reqHandler;

        /// <summary>
        /// List of RouteLayerInfo
        /// </summary>
        private List<RouteLayerInfo> routeLayerInfos;

        /// <summary>
        /// Initializes a new instance of the <see cref="DSUtility"/> class
        /// </summary>
        public DSUtility()
        {
            this.soeName = this.GetType().Name;
            this.reqHandler = new SoeRestImpl(this.soeName, this.CreateRestSchema()) as IRESTRequestHandler;
        }

        #region IServerObjectExtension Members
        /// <summary>
        /// init event of soe
        /// </summary>
        /// <param name="pSOH">Helper ServerObject</param>
        public void Init(IServerObjectHelper pSOH)
        {
            this.serverObjectHelper = pSOH;
        }

        /// <summary>
        /// shutdown event of soe
        /// </summary>
        public void Shutdown()
        {
        }

        #endregion

        #region IObjectConstruct Members

        /// <summary>
        /// costruct event of soe
        /// </summary>
        /// <param name="props">properties of soe</param>
        public void Construct(IPropertySet props)
        {
            this.routeLayerInfos = new List<RouteLayerInfo>();
            this.GetRouteLayerInfos();
        }

        #endregion

        #region IRESTRequestHandler Members

        /// <summary>
        /// get schema of soe
        /// </summary>
        /// <returns>schema of soe</returns>
        public string GetSchema()
        {
            return this.reqHandler.GetSchema();
        }

        /// <summary>
        /// Handler of request rest
        /// </summary>
        /// <param name="capabilities">capabilities of soe</param>
        /// <param name="resourceName">name of resource</param>
        /// <param name="operationName">name of operation</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        public byte[] HandleRESTRequest(string capabilities, string resourceName, string operationName, string operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            return this.reqHandler.HandleRESTRequest(capabilities, resourceName, operationName, operationInput, outputFormat, requestProperties, out responseProperties);
        }

        #endregion

        /// <summary>
        /// Get from input of operation the routeIDFieldName
        /// </summary>
        /// <param name="operationInput">input of operation</param>
        /// <param name="routeIDFieldNameDefault">value of default if routeIDFieldName is missing</param>
        /// <returns>value of RouteIDFieldName</returns>
        private static string GetRouteIDFieldName(JsonObject operationInput, string routeIDFieldNameDefault)
        {
            string routeIDFieldNameValue;
            bool found = operationInput.TryGetString("routeIDFieldName", out routeIDFieldNameValue);
            if (!found || string.IsNullOrEmpty(routeIDFieldNameValue))
            {
                if (routeIDFieldNameDefault == null)
                {
                    throw new DynamicSegmentationException("routeIDFieldName");
                }
                else
                {
                    routeIDFieldNameValue = routeIDFieldNameDefault;
                }
            }

            return routeIDFieldNameValue;
        }

        /// <summary>
        /// Get from input of operation the routeIDFieldName
        /// </summary>
        /// <param name="operationInput">input of operation</param>
        /// <returns>value of RouteIDFieldName</returns>
        private static string GetRouteIDFieldName(JsonObject operationInput)
        {
            return DSUtility.GetRouteIDFieldName(operationInput, null);
        }

        /// <summary>
        /// Get from input of operation the routeMeasureUnit
        /// </summary>
        /// <param name="operationInput">input of operation</param>
        /// <returns>value of routeMeasureUnit</returns>
        private static esriUnits GetMeasureUnit(JsonObject operationInput)
        {
            string routeMeasureUnitValue;
            esriUnits routeMeasureUnit = esriUnits.esriUnknownUnits;
            bool found = operationInput.TryGetString("routeMeasureUnit", out routeMeasureUnitValue);
            if (found && !string.IsNullOrEmpty(routeMeasureUnitValue))
            {
                if (Enum.IsDefined(typeof(esriUnits), routeMeasureUnitValue))
                {
                    routeMeasureUnit = (esriUnits)Enum.Parse(typeof(esriUnits), routeMeasureUnitValue, true);
                }
            }

            return routeMeasureUnit;
        }

        /// <summary>
        /// get RouteLocator
        /// </summary>
        /// <param name="featureClass">object feature class</param>
        /// <param name="routeIDFieldNameValue">value of routeIDFieldName</param>
        /// <param name="routeMeasureUnit">unit of route measure</param>
        /// <returns>object IRouteLocator2</returns>
        private static IRouteLocator2 GetRouteLocator(IFeatureClass featureClass, string routeIDFieldNameValue, esriUnits routeMeasureUnit)
        {
            IDataset dataset = featureClass as IDataset;
            IName nameDataset = dataset.FullName;
            IRouteLocatorName routeMeasureLocatorName = new RouteMeasureLocatorNameClass();
            routeMeasureLocatorName.RouteFeatureClassName = nameDataset;
            routeMeasureLocatorName.RouteIDFieldName = routeIDFieldNameValue;
            routeMeasureLocatorName.RouteMeasureUnit = routeMeasureUnit;
            routeMeasureLocatorName.RouteWhereClause = string.Empty;
            nameDataset = (IName)routeMeasureLocatorName;
            return nameDataset.Open() as IRouteLocator2;
        }

        /// <summary>
        /// get RouteLayerInfo from Id
        /// </summary>
        /// <param name="routeLayerID">value of routeLayerID</param>
        /// <returns>object RouteLayerInfo</returns>
        private RouteLayerInfo GetRouteLayerInfo(int routeLayerID)
        {
            if (routeLayerID < 0)
            {
                throw new ArgumentOutOfRangeException("routeLayerID");
            }

            IMapServer3 serverObject = this.GetMapServer();
            IMapLayerInfos mapLayerInfos = serverObject.GetServerInfo(serverObject.DefaultMapName).MapLayerInfos;
            long count = mapLayerInfos.Count;
            for (int i = 0; i < count; i++)
            {
                IMapLayerInfo mapLayerInfo = mapLayerInfos.get_Element(i);
                if (mapLayerInfo.ID == routeLayerID)
                {
                    return new RouteLayerInfo(mapLayerInfo);
                }
            }

            throw new ArgumentOutOfRangeException("routeLayerID");
        }

        /// <summary>
        /// Feature Class from id of layer
        /// </summary>
        /// <param name="routelayerID">id route layer</param>
        /// <returns>feature class</returns>
        private IFeatureClass GetRouteFeatureClass(int routelayerID)
        {
            IMapServer3 mapServer = this.GetMapServer();
            IMapServerDataAccess dataAccess = (IMapServerDataAccess)mapServer;
            return (IFeatureClass)dataAccess.GetDataSource(mapServer.DefaultMapName, routelayerID);
        }

        /// <summary>
        /// From service fills list of layer with M
        /// </summary>
        private void GetRouteLayerInfos()
        {
            IMapServer3 serverObject = this.GetMapServer();
            IMapLayerInfos mapLayerInfos = serverObject.GetServerInfo(serverObject.DefaultMapName).MapLayerInfos;

            this.routeLayerInfos = new List<RouteLayerInfo>();
            for (int i = 0; i < mapLayerInfos.Count; i++)
            {
                IMapLayerInfo mapLayerInfo = mapLayerInfos.get_Element(i);
                if (mapLayerInfo.IsFeatureLayer)
                {
                    IFields fields = mapLayerInfo.Fields;
                    for (int j = 0; j < fields.FieldCount; j++)
                    {
                        IField field = fields.get_Field(j);
                        if (field.Type == esriFieldType.esriFieldTypeGeometry)
                        {
                            IGeometryDef geometryDef = field.GeometryDef;
                            if (geometryDef.HasM)
                            {
                                this.routeLayerInfos.Add(new RouteLayerInfo(mapLayerInfo));
                            }
                            break;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Get object MapServer of ServerObject 
        /// </summary>
        /// <returns>object MapServer</returns>
        private IMapServer3 GetMapServer()
        {
            IMapServer3 mapServer = this.serverObjectHelper.ServerObject as IMapServer3;
            if (mapServer == null)
            {
                throw new DynamicSegmentationException("Unable to access the map server.");
            }

            return mapServer;
        }

        /// <summary>
        /// create schema of soe
        /// </summary>
        /// <returns>resource of soe</returns>
        private RestResource CreateRestSchema()
        {
            RestResource rootRes = new RestResource(this.soeName, false, this.RootResHandler);
            RestResource item = new RestResource("RouteLayers", true, new ResourceHandler(this.RouteLayer));
            
            RestOperation pointLocation = new RestOperation("PointLocation", new string[] { "routeIDFieldName", "routeID", "measure", "lateralOffset", "routeMeasureUnit" }, new string[] { "json" }, this.PointLocationOperHandler, "Point Location");
            RestOperation lineLocation = new RestOperation("LineLocation", new string[] { "routeIDFieldName", "routeID", "fromMeasure", "toMeasure", "lateralOffset", "routeMeasureUnit" }, new string[] { "json" }, this.LineLocationOperHandler, "Line Location");
            RestOperation identifyRoute = new RestOperation("IdentifyRoute", new string[] { "location", "tolerance", "routeMeasureUnit", "routeIDFieldName" }, new string[] { "json" }, this.IdentifyRouteOperHandler, "Identify Route");
            
            item.operations.Add(pointLocation);
            item.operations.Add(lineLocation);
            item.operations.Add(identifyRoute);
            rootRes.resources.Add(item);
            return rootRes;
        }

        /// <summary>
        /// handler of resource root
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>root resource in format output in byte</returns>
        private byte[] RootResHandler(NameValueCollection boundVariables, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = null;
            JsonObject[] objectArray = System.Array.ConvertAll(this.routeLayerInfos.ToArray(), i => i.ToJsonObject());
            JsonObject jsonObject = new JsonObject();
            jsonObject.AddArray("routeLayers", objectArray);
            return jsonObject.JsonByte();
        }

        /// <summary>
        /// resource RouteLayer
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>resource in byte</returns>
        private byte[] RouteLayer(NameValueCollection boundVariables, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = null;
            int layerID = Convert.ToInt32(boundVariables["RouteLayersID"], CultureInfo.InvariantCulture);
            return this.GetRouteLayerInfo(layerID).ToJsonObject().JsonByte();
        }

        /// <summary>
        /// Handler operation Point Location
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        private byte[] PointLocationOperHandler(NameValueCollection boundVariables, JsonObject operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = null;
            
            int routeLayerID = Convert.ToInt32(boundVariables["RouteLayersID"], CultureInfo.InvariantCulture);
            
            esriUnits routeMeasureUnit = DSUtility.GetMeasureUnit(operationInput);
            
            double? measure;
            bool found = operationInput.TryGetAsDouble("measure", out measure);
            if (!found || !measure.HasValue)
            {
                throw new DynamicSegmentationException("measure not valid");
            }

            double? lateralOffset;
            found = operationInput.TryGetAsDouble("lateralOffset", out lateralOffset);
            if (!found || !lateralOffset.HasValue)
            {
                lateralOffset = 0;
            }

            IFeatureClass featureClass = this.GetRouteFeatureClass(routeLayerID);

            string routeIDFieldNameValue = DSUtility.GetRouteIDFieldName(operationInput);
            
            IFields fields = featureClass.Fields;
            int indexField = fields.FindField(routeIDFieldNameValue);
            if (indexField == -1)
            {
                throw new DynamicSegmentationException(string.Format(CultureInfo.InvariantCulture, "routeIDFieldName {0} not found!", routeIDFieldNameValue));
            }

            object routeID;
            found = operationInput.TryGetObject("routeID", out routeID);
            if (!found)
            {
                throw new DynamicSegmentationException("routeID not valid");
            }

            IField field = fields.get_Field(indexField);
            if ((field.Type == esriFieldType.esriFieldTypeInteger) || (field.Type == esriFieldType.esriFieldTypeOID))
            {
                try 
                {
                   int.Parse(routeID.ToString(), CultureInfo.InvariantCulture);
                }
                catch
                {
                    throw new FormatException("routeID must be integer!");
                }
            }
            else if (field.Type == esriFieldType.esriFieldTypeDouble)
            {
                try 
                {
                    double.Parse(routeID.ToString(), CultureInfo.InvariantCulture);
                }
                catch
                {
                    throw new FormatException("routeID must be double!");
                }
            }
            else if (field.Type == esriFieldType.esriFieldTypeString)
            {
            }
            else
            {
                throw new DynamicSegmentationException(string.Format(CultureInfo.InvariantCulture, "Field Type routeIDFieldName must be string, double or integer!", routeIDFieldNameValue));
            }

            IRouteLocator2 routeLocator = DSUtility.GetRouteLocator(featureClass, routeIDFieldNameValue, routeMeasureUnit);

            IRouteLocation routeLocation = new RouteMeasurePointLocationClass();
            routeLocation.RouteID = routeID;
            routeLocation.MeasureUnit = routeLocator.MeasureUnit;
            routeLocation.LateralOffset = lateralOffset.Value;

            IRouteMeasurePointLocation routeMeasurePointLocation = (IRouteMeasurePointLocation)routeLocation;
            routeMeasurePointLocation.Measure = measure.Value;

            IGeometry geometry;
            esriLocatingError locatingError;
            routeLocator.Locate(routeLocation, out geometry, out locatingError);
            int errorId = (int)locatingError;

            JsonObject result = null;

            if (errorId != 0)
            {
                string errorDescription = string.Format(CultureInfo.InvariantCulture, "{0} ({1})", Enum.GetName(typeof(esriLocatingError), locatingError), errorId);
                IObjectJson jsonObjectError = new ObjectError(errorDescription);
                result = jsonObjectError.ToJsonObject();
            }

            if ((geometry != null) && (!geometry.IsEmpty))
            {
                if (result == null)
                {
                    result = new JsonObject();
                }

                result.AddJsonObject("geometries", Conversion.ToJsonObject(geometry));
            }
            
            return result.JsonByte();
        }

        /// <summary>
        /// Handler operation Line Location
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        private byte[] LineLocationOperHandler(NameValueCollection boundVariables, JsonObject operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = null;
            int routeLayerID = Convert.ToInt32(boundVariables["RouteLayersID"], CultureInfo.InvariantCulture);

            esriUnits routeMeasureUnit = DSUtility.GetMeasureUnit(operationInput);

            double? fromMeasure;
            operationInput.TryGetAsDouble("fromMeasure", out fromMeasure);
          
            double? toMeasure;
            operationInput.TryGetAsDouble("toMeasure", out toMeasure);
            
            if (!fromMeasure.HasValue && !toMeasure.HasValue)
            {
                throw new DynamicSegmentationException("Set at least fromMeasure or toMeasure!");
            }

            double? lateralOffset;
            bool found = operationInput.TryGetAsDouble("lateralOffset", out lateralOffset);
            if (!found || !lateralOffset.HasValue)
            {
                lateralOffset = 0;
            }

            IFeatureClass featureClass = this.GetRouteFeatureClass(routeLayerID);

            string routeIDFieldNameValue = DSUtility.GetRouteIDFieldName(operationInput);

            IFields fields = featureClass.Fields;
            int indexField = fields.FindField(routeIDFieldNameValue);
            if (indexField == -1)
            {
                throw new DynamicSegmentationException(string.Format(CultureInfo.InvariantCulture, "routeIDFieldName {0} not found!", routeIDFieldNameValue));
            }

            object routeID;
            found = operationInput.TryGetObject("routeID", out routeID);
            if (!found)
            {
                throw new DynamicSegmentationException("routeID not valid");
            }

            IField field = fields.get_Field(indexField);
            if ((field.Type == esriFieldType.esriFieldTypeInteger) || (field.Type == esriFieldType.esriFieldTypeOID))
            {
                try
                {
                    int.Parse(routeID.ToString(), CultureInfo.InvariantCulture);
                }
                catch
                {
                    throw new FormatException("routeID must be integer!");
                }
            }
            else if (field.Type == esriFieldType.esriFieldTypeDouble)
            {
                try
                {
                    double.Parse(routeID.ToString(), CultureInfo.InvariantCulture);
                }
                catch
                {
                    throw new FormatException("routeID must be double!");
                }
            }
            else if (field.Type == esriFieldType.esriFieldTypeString)
            {
            }
            else
            {
                throw new DynamicSegmentationException(string.Format(CultureInfo.InvariantCulture, "Field Type routeIDFieldName must be string, double or integer!", routeIDFieldNameValue));
            }

            IRouteLocator2 routeLocator = DSUtility.GetRouteLocator(featureClass, routeIDFieldNameValue, routeMeasureUnit);
            
            IRouteLocation routeLocation = new RouteMeasureLineLocationClass();
            routeLocation.RouteID = routeID;
            routeLocation.MeasureUnit = routeLocator.MeasureUnit;
            routeLocation.LateralOffset = lateralOffset.Value;

            IRouteMeasureLineLocation routeMeasureLineLocation = (IRouteMeasureLineLocation)routeLocation;
            if (fromMeasure.HasValue)
            {
                routeMeasureLineLocation.FromMeasure = fromMeasure.Value;
            }

            if (toMeasure.HasValue)
            {
                routeMeasureLineLocation.ToMeasure = toMeasure.Value;
            }
            
            IGeometry geometry;
            esriLocatingError locatingError;
            routeLocator.Locate(routeLocation, out geometry, out locatingError);
            int errorId = (int)locatingError;

            JsonObject result = null;

            if (errorId != 0)
            {
                string errorDescription = string.Format(CultureInfo.InvariantCulture, "{0} ({1})", Enum.GetName(typeof(esriLocatingError), locatingError), errorId);
                IObjectJson jsonObjectError = new ObjectError(errorDescription);
                result = jsonObjectError.ToJsonObject();
            }

            if ((geometry != null) && (!geometry.IsEmpty))
            {
                if (result == null)
                {
                    result = new JsonObject();
                }

                result.AddJsonObject("geometries", Conversion.ToJsonObject(geometry));
            }

            return result.JsonByte();
        }
        
        /// <summary>
        /// Handler operation Identify Route
        /// </summary>
        /// <param name="boundVariables">list of variables bound</param>
        /// <param name="operationInput">input of operation</param>
        /// <param name="outputFormat">format of output</param>
        /// <param name="requestProperties">list of request properties</param>
        /// <param name="responseProperties">list of response properties </param>
        /// <returns>response in byte</returns>
        private byte[] IdentifyRouteOperHandler(NameValueCollection boundVariables, JsonObject operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
                responseProperties = null;
                string methodName = MethodBase.GetCurrentMethod().Name;
                int routeLayerID = Convert.ToInt32(boundVariables["RouteLayersID"], CultureInfo.InvariantCulture);

                esriUnits routeMeasureUnit = DSUtility.GetMeasureUnit(operationInput);
                IFeatureClass featureClass = this.GetRouteFeatureClass(routeLayerID);
                string routeIDFieldNameValue = DSUtility.GetRouteIDFieldName(operationInput, featureClass.OIDFieldName);
                IRouteLocator2 routeLocator = DSUtility.GetRouteLocator(featureClass, routeIDFieldNameValue, routeMeasureUnit);

                double? tolerance;
                bool found = operationInput.TryGetAsDouble("tolerance", out tolerance);
                if (!found || !tolerance.HasValue)
                {
                    tolerance = 0.0;
                }

                JsonObject jsonLocation;
                if (!operationInput.TryGetJsonObject("location", out jsonLocation))
                {
                    throw new ArgumentException("Invalid location", methodName);
                }
              
                IPoint location = Conversion.ToGeometry(jsonLocation, esriGeometryType.esriGeometryPoint) as IPoint;
                if (location == null)
                {
                    throw new ArgumentException("Invalid location", methodName);
                }

                IEnvelope envelope = location.Envelope;
                envelope.Expand(tolerance.Value, tolerance.Value, false);
                IRouteMeasurePointLocation routeMeasurePointLocation = new RouteMeasurePointLocationClass();
                IRouteLocation routeLocation;           
                IFeature feature;           
                JsonObject result = new JsonObject();
                
                List<JsonObject> measures = new List<JsonObject>();
                IEnumRouteIdentifyResult enumResult = routeLocator.Identify(envelope, string.Empty);           
                for (int i = 1; i == enumResult.Count; i++)            
                {
                    enumResult.Next(out routeLocation, out feature);
                    routeMeasurePointLocation = (IRouteMeasurePointLocation)routeLocation;
                    JsonObject measure = new JsonObject();
                    measure.AddString("routeID", routeLocation.RouteID.ToString());
                    measure.AddDouble("measure", routeMeasurePointLocation.Measure);
                    measures.Add(measure);       
                }

                result.AddArray("location", measures.ToArray());

                return result.JsonByte();
        }
    }
}